package flare.display
{
  import flash.display.DisplayObject;
  import flash.display.Sprite;
  import flash.display.Stage;
  import flash.events.Event;
  import flash.geom.Point;
  import flash.geom.Rectangle;

  /**
   * A Sprite that redraws itself as needed when marked as "dirty".
   * This allows multiple changes to be made to the sprite between frames
   * without triggering a potentially costly redraw for each property update.
   * Instead, the <code>dirty()</code> method should be called whenever a
   * change is made to the Sprite that would normally require a redraw. This
   * class will ensure that the Sprite is redrawn only once before the next
   * frame is rendered.
   * 
   * <p>Subclasses should place drawing code within the <code>render()</code>
   * method. For all properties used by the <code>render</code> method to help
   * draw this sprite, the corresponding "setter" method should call the
   * <code>dirty()</code> method to mark the sprite as dirty and thereby
   * trigger a redraw for the next frame.</p>
   * 
   * <p>Internally, the DirtySprite class maintains a static list of all
   * "dirty" sprites, and redraws each sprite in the list when a
   * <code>Event.RENDER</code> event is issued. Typically, this process is
   * performed automatically. In a few cases, erratic behavior has been
   * observed due to a Flash Player bug that results in <code>RENDER</code>
   * events not being properly issued. As a fallback, the static
   * <code>renderDirty()</code> method can be invoked to manually force
   * each dirty sprite to be redrawn.</p>
   */
  public class DirtySprite extends Sprite
  {
    private static var __stage:Stage;
    private static var __installed:Boolean = false;
    private static var __dirtyList:Vector.<Object> = new Vector.<Object>();

    /**
     * Installs the frame render listener on the stage.
     */
    private static function install(stage:Stage):void
    {
      __stage = stage;
      __stage.addEventListener(Event.RENDER, renderDirty);
      __installed = true;
    }

    /**
     * Frame render callback that renders all sprites on the dirty list.
     * Typically, this method is automatically triggered by stage
     * RENDER events. It can also be manually invoked to redraw all
     * dirty DirtySprites.
     * @param evt the event that triggered the rendering (can be null)
     */
    public static function renderDirty(evt:Event = null):void
    {
      while(__dirtyList.length > 0)
      {
        var ds:DirtySprite = DirtySprite(__dirtyList.pop());
        var db:Boolean = (ds._dirty == DIRTY);
        ds._dirty = CLEAN;

        if(db)ds.render();
      }

      // We need to remove and then re-add the listeners
      // to work around Flash Player bugs (#139381?). Ugh.
      // TODO: it seems this is not a complete solution, as in
      // rare cases RENDER events are still omitted.
      if(__stage != null)
      {
        __stage.removeEventListener(Event.RENDER, renderDirty);
        __installed = false;
      }
    }

    // --------------------------------------------------------------------

    private static const CLEAN:int = 0; // no changes
    private static const DIRTY:int = 1; // re-rendering needed
    private static const VISIT:int = 2; // was re-rendered, but on list

    /** @private */
    protected var _dirty:int = DIRTY; // dirty at birth

    /**
     * Creates a new DirtySprite. Registers this Sprite to receive
     * added-to-stage events.
     */
    public function DirtySprite()
    {
      this.addEventListener(Event.ADDED_TO_STAGE, onAddToStage, false, 0, true); // use weak reference
      __dirtyList.push(this);
    }

    /**
     * Makes sure that "dirtying" changes made to this Sprite
     * while it is off the display list still result in a
     * re-rendering if the Sprite is ever added to the list.
     */
    private function onAddToStage(evt:Event):void
    {
      if(_dirty == DIRTY)
      {
        if(!__installed)install(stage);
        stage.invalidate();
      }
    }

    /**
     * Marks this sprite as "dirty" and in need of re-rendering.
     * The next time that (a) a new frame is rendered, and
     * (b) this Sprite is on the display list, the render method
     * will automatically be called.
     */
    public final function dirty():void
    {
      if(_dirty == DIRTY)return;

      __dirtyList.push(this);

      if(stage)
      {
        if(!__installed)install(stage);
        stage.invalidate();
      }
      _dirty = DIRTY;
    }

    /**
     * If dirty, this sprite is re-rendered before returning the width.
     */
    public override function get width():Number
    {
      if(_dirty == DIRTY)
      {
        _dirty = VISIT;
        render();
      }
      return super.width;
    }

    /**
     * If dirty, this sprite is re-rendered before returning the height.
     */
    public override function get height():Number
    {
      if(_dirty == DIRTY)
      {
        _dirty = VISIT;
        render();
      }
      return super.height;
    }

    /**
     * If dirty, this sprite is re-rendered before returning the rect.
     */
    public override function getRect(targetCoordinateSpace:DisplayObject):Rectangle
    {
      if(_dirty == DIRTY)
      {
        _dirty = VISIT;
        render();
      }
      return super.getRect(targetCoordinateSpace);
    }

    /**
     * If dirty, this sprite is re-rendered returning the bounds.
     */
    public override function getBounds(targetCoordinateSpace:DisplayObject):Rectangle
    {
      if(_dirty == DIRTY)
      {
        _dirty = VISIT;
        render();
      }
      return super.getBounds(targetCoordinateSpace);
    }

    /**
     * If dirty, either sprite is re-rendered before hit-testing.
     */
    public override function hitTestObject(obj:DisplayObject):Boolean
    {
      if(_dirty == DIRTY)
      {
        _dirty = VISIT;
        render();
      }
      var ds:DirtySprite = obj as DirtySprite;

      if(ds && ds._dirty == DIRTY)
      {
        ds._dirty = VISIT;
        ds.render();
      }
      return super.hitTestObject(obj);
    }

    /**
     * If dirty, this sprite is re-rendered before hit-testing.
     */
    public override function hitTestPoint(x:Number, y:Number, shapeFlag:Boolean = false):Boolean
    {
      if(_dirty == DIRTY)
      {
        _dirty = VISIT;
        render();
      }
      return super.hitTestPoint(x, y, shapeFlag);
    }

    /**
     * Draw this sprite's graphical content. Subclasses should
     * override this method with custom drawing code.
     */
    public function render():void
    {
    // for sub-classes to override...
    }

    /** @inheritDoc */
    public override function toString():String
    {
      var s:String = super.toString();
      return name == null ? s : s + " \"" + name + "\"";
    }


    // -- Polar Coordinates -----------------------------------------------

    /** A constant for the point (0,0). */
    public static const ZERO:Point = new Point(0, 0);

    /** The radius value of this sprite's position in polar co-ordinates.
     *  Polar coordinate values are assume a circle center given by the
     *  <code>origin</code> property. */
    protected var _radius:Number;

    /** The angle value of this sprite's position in polar co-ordinates.
     *  Polar coordinate values are assume a circle center given by the
     *  <code>origin</code> property. */
    protected var _angle:Number;

    /** The origin point for polar coordinates. */
    protected var _origin:Point = ZERO;

    /** @inheritDoc */
    public override function set x(v:Number):void
    {
      super.x = v;
      _radius = NaN;
      _angle = NaN;
    }
    /** @inheritDoc */
    public override function set y(v:Number):void
    {
      super.y = v;
      _radius = NaN;
      _angle = NaN;
    }

    /** The radius value of this sprite's position in polar co-ordinates.
     *  Polar coordinate values are assume a circle center given by the
     *  <code>origin</code> property. */
    public function get radius():Number
    {
      if(isNaN(_radius))
      {
        var cx:Number = x - _origin.x;
        var cy:Number = y - _origin.y;
        _radius = Math.sqrt(cx * cx + cy * cy);
      }
      return _radius;
    }

    public function set radius(r:Number):void
    {
      var a:Number = angle;
      super.x = r * Math.cos(a) + _origin.x;
      super.y = - r * Math.sin(a) + _origin.y;
      _radius = r;
    }

    /** The angle value of this sprite's position in polar co-ordinates.
     *  Polar coordinate values are assume a circle center given by the
     *  <code>origin</code> property. */
    public function get angle():Number
    {
      if(isNaN(_angle))
      {
        _angle = Math.atan2(-(y - _origin.y), (x - _origin.x));
      }
      return _angle;
    }

    public function set angle(a:Number):void
    {
      var r:Number = radius;
      super.x = r * Math.cos(a) + _origin.x;
      super.y = - r * Math.sin(a) + _origin.y;
      _angle = a;
    }

    /** The origin point for polar coordinates. */
    public function get origin():Point
    {
      return _origin;
    }

    public function set origin(p:Point):void
    {
      if(p.x != _origin.x || p.y != _origin.y)
      {
        _radius = NaN;
        _angle = NaN;
      }
      _origin = p;
    }
  } // end of class DirtySprite
}